import _ from 'lodash';
import React, {useEffect, useRef, useState} from 'react';
import MapNode from '../../types/MapNode';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons/faQuestionCircle';
import IslandHttpClient, {APIEndpoint} from '../../IslandHttpClient';
import LoadingIcon from '../../ui-components/LoadingIcon';


type ExploitationAttempt = {
  source: string;
  success: boolean;
  timestamp: Date;
  exploiterName: string;
}

type ExploitationEvent = {
  source: string;
  success: boolean;
  timestamp: number;
  exploiter_name: string;
  target: string;
}

const ExploitationTimeline = (props: { node: MapNode, allNodes: MapNode[] }) => {

  const [exploitationAttempts, setExploitationAttempts] = useState<ExploitationAttempt[]>([]);
  const [loadingEvents, setLoadingEvents] = useState(true);
  const [updateInProgress, setUpdateInProgress] = useState(false);
  const prevNodeRef = useRef<MapNode>(null);

  const getExploitationAttempts = (node: MapNode) => {
    let url_args = {'type': 'ExploitationEvent'};
    return IslandHttpClient.getJSON(APIEndpoint.agentEvents, url_args, updateInProgress)
      .then(res => res.body).then(events => {
        return parseEvents(events, node);
      })
  }

  const updateAttemptsFromServer = () => {
    setUpdateInProgress(true);
    return getExploitationAttempts(props.node).then((attempts) => {
        if(props.node === prevNodeRef.current){
          setExploitationAttempts(attempts);
          return true;
        } else {
          return false;
        }
      }
    );
  }

  useEffect(() => {
    // TODO: A better solution is probably to retrieve all events store them in this component. When
    //       the pane switches, you can select relevant events from the set of all events. A timer
    //       can be used to periodically update the set of stored events.
    const nodeChanged = Boolean(props.node !== prevNodeRef.current);
    prevNodeRef.current = props.node;
    if(nodeChanged){
      setLoadingEvents(true);
      setUpdateInProgress(true);
      updateAttemptsFromServer()
        .then((updated) => {
          if(updated){
            setUpdateInProgress(false);
            setLoadingEvents(false);
          }});
    } else if(!updateInProgress) {
      setUpdateInProgress(true);
      let oneSecond = 1000;
      new Promise(r => setTimeout(r, oneSecond * 3))
        .then(() => updateAttemptsFromServer())
        .then((updated) => {
          if(updated){
            setUpdateInProgress(false);
            setLoadingEvents(false);
          }});
    }
  }, [updateInProgress, props.node]);

  function parseEvents(events: ExploitationEvent[], node: MapNode): ExploitationAttempt[] {
    let exploitationAttempts = [];
    let filteredEvents = events.filter(event => node.hasIp(event.target))
    for (const event of Object.values(filteredEvents)) {
      let iface = node.networkInterfaces.find(iface => iface.includes(event.target))
      if (iface !== undefined) {
        let timestampInMilliseconds: number = event.timestamp * 1000;
        exploitationAttempts.push({
          source: getSourceNodeLabel(event.source),
          success: event.success,
          timestamp: new Date(timestampInMilliseconds),
          exploiterName: event.exploiter_name
        });
      }
    }
    return exploitationAttempts.sort((a, b) => a.timestamp - b.timestamp);
  }

  function getAttemptList(): any {
    if (exploitationAttempts.length === 0) {
      return (<li className={'timeline-content'}>No exploits were attempted on this node yet.</li>);
    } else {
      return aggregateExploitationAttempts(_.sortBy(exploitationAttempts, 'timestamp'))
        .map(data => {
          const {data: attempt, count: count} = data;
          return (
            <li key={`${attempt.timestamp}${String(Math.random())}`}>
              <div className={'bullet ' + (attempt.success ? 'bad' : '')}>
                <div className={'event-count'}>{count < 100 ? count : '99+'}</div>
              </div>
              <div className={'timeline-content'}>
                <div>{new Date(attempt.timestamp).toLocaleString()}</div>
                <div>{attempt.source}</div>
                <div>{attempt.exploiterName}</div>
              </div>
            </li>
          );
        })
    }
  }

  function getSourceNodeLabel(agentId: string): string {
    try {
      return props.allNodes.filter(node => node.agentIds.includes(agentId))[0].getLabel()
    } catch {
      return 'Unknown'
    }
  }

  return (
    <div className={'exploit-timeline'}>
      <h4 style={{'marginTop': '2em'}}>
        Exploit Timeline&nbsp;
        {generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')}
      </h4>
      {loadingEvents ? <LoadingIcon/> :
        <ul className="timeline">
          {getAttemptList()}
        </ul>
      }
    </div>
  )
}


function generateToolTip(text) {
  return (
    <OverlayTrigger placement="top"
                    overlay={<Tooltip id="tooltip">{text}</Tooltip>}
                    delay={{show: 250, hide: 400}}>
      <a><FontAwesomeIcon icon={faQuestionCircle} style={{'marginRight': '0.5em'}}/></a>
    </OverlayTrigger>
  );
}

function aggregateExploitationAttempts(attempts) {
  let aggregatedAttempts = [];

  for (const attempt of attempts) {
    let len = aggregatedAttempts.length;
    if (len > 0 && areAttemptsIdentical(attempt, aggregatedAttempts[len - 1].data)) {
      aggregatedAttempts[len - 1].count++;
    } else {
      aggregatedAttempts.push({data: _.cloneDeep(attempt), count: 1});
    }
  }

  return aggregatedAttempts;
}

function areAttemptsIdentical(attemptOne: ExploitationAttempt, attemptTwo: ExploitationAttempt) {
  return ((attemptOne.source === attemptTwo.source) &&
    (attemptOne.exploiterName === attemptTwo.exploiterName) &&
    (attemptOne.success === attemptTwo.success))
}

export default ExploitationTimeline
